import {
  type PropType,
  computed,
  defineComponent,
  onMounted,
  ref,
  watchEffect,
} from 'vue'
import { round } from 'lodash-es'

export interface TweenProps {
  from: number
  to: number
  duration: number
  onUpdate: (currentValue: number) => void
  onFinish: () => void
}
const easeOut = (t: number): number => 1 - (1 - t) ** 5
export function tween(props: TweenProps): void {
  const { from, to, duration, onUpdate, onFinish } = props
  const startTime = performance.now()
  const tick = (): void => {
    const current = performance.now()
    const elapsedTime = Math.min(current - startTime, duration)
    const currentValue = from + (to - from) * easeOut(elapsedTime / duration)
    if (elapsedTime === duration) {
      onFinish()
      return
    }
    onUpdate(currentValue)
    requestAnimationFrame(tick)
  }
  tick()
}

export const numberAnimationProps = {
  to: {
    type: Number,
    default: 0,
  },
  precision: {
    type: Number,
    default: 0,
  },
  showSeparator: Boolean,
  locale: String,
  from: { type: Number, default: 0 },
  active: {
    type: Boolean,
    default: true,
  },
  duration: {
    type: Number,
    default: 2000,
  },
  onFinish: Function as PropType<() => void>,
}

export type NumberAnimationProps = typeof numberAnimationProps

export interface NumberAnimationInst {
  play: () => void
}

export default defineComponent({
  name: 'NumberAnimation',
  props: numberAnimationProps,
  setup(props) {
    const { duration } = props
    const displayedValueRef = ref(props.from)

    let animating = false
    const onUpdate = (currentValue: number): void => {
      displayedValueRef.value = currentValue
    }
    const onFinish = (): void => {
      displayedValueRef.value = props.to
      animating = false
      props.onFinish?.()
    }
    const animate = (
      from: number = props.from,
      to: number = props.to,
    ): void => {
      animating = true
      displayedValueRef.value = props.from
      if (from !== to) {
        tween({
          from,
          to,
          duration,
          onUpdate,
          onFinish,
        })
      }
    }
    const formattedValueRef = computed(() => {
      const formatted: string = round(
        displayedValueRef.value,
        props.precision,
      ).toFixed(props.precision)
      const splitValue = formatted?.split?.('.')
      const numberFormatter = new Intl.NumberFormat()
      const decimalSeparator = numberFormatter
        .formatToParts(0.5)
        .find(part => part.type === 'decimal')?.value
      const integer = splitValue[0]
      const decimal = splitValue[1]
      return {
        integer,
        decimal,
        decimalSeparator,
      }
    })
    function play(): void {
      if (animating)
        return
      animate()
    }
    onMounted(() => {
      watchEffect(() => {
        if (props.active)
          animate()
      })
    })
    const exposedMethods: NumberAnimationInst = { play }
    return {
      formattedValue: formattedValueRef,
      ...exposedMethods,
    }
  },
  render() {
    const {
      formattedValue: { integer, decimal, decimalSeparator },
    } = this
    return [integer, decimal ? decimalSeparator : null, decimal]
  },
})
